{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true,
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# 10.PySpyder 框架爬取专题\n",
    "\n",
    "## 1. 框架介绍\n",
    "\n",
    "我们将之前爬虫的各个组件独立归来，定义成不同的模块，\n",
    "也就慢慢形成了一个框架 有了框架之后，我们就不必关心爬虫的全部流程，异常处理、任务调度等\n",
    "集成在框架中。我们只需要关心爬虫的核心逻辑部分即可，如页面信息的提取、下一步请求的生\n",
    "成等。 这样，不仅开发效率会提高很多，而且爬虫的健壮性也更强。\n",
    "\n",
    "在项目实战过程中，我们往往会采用爬虫框架来实现抓取，这样可提升开发效率、节省开发时间\n",
    "pyspider 就是非常优秀的爬虫框架，它的操作便捷、功能强大，利用它我们可以快速方便地完成\n",
    "虫的开发,特点如下：\n",
    "\n",
    "**1.提供WebUI界面，调试爬虫很方便\n",
    "2.可以很方便的进行爬取的流程监控和爬虫项目管理\n",
    "3.支持常见的数据库\n",
    "4.支持使用PhantomJS，可以抓取JavaScript页面\n",
    "5.支持优先级定制和定时爬取等功能**\n",
    "\n",
    "pyspider 带有强大的 WebUI 、脚本编辑器、任务监控器、项目管理器以及结果处理器，它支持多\n",
    "种数据库后端 多种消息队列、 JavaScript 渲染页面的爬取，使用起来非常方便\n",
    "\n",
    "![](PyspiderFrame.jpg)\n",
    "\n",
    "每个pyspider的项目对应一个Python脚本，该脚本中定义了一个handler类，\n",
    "它有on_start()方法 爬取首先调用on_start()方法生成最初的抓取任务，\n",
    "然后发送给Scheduler进行调度\n",
    "\n",
    "Scheduler将抓取任务分发给Fetcher进行抓取，Fetcher执行并得到响应，随后将响应发送给\n",
    "Processer\n",
    "\n",
    "Processer处理响应并提取新的URL生成新的抓取任务，然后通过消息队列的方式通知\n",
    "Schduler当前抓取任务执行情况，并将新生成的抓取任务发送给Scheduler\n",
    "\n",
    "如果生成了新的提取结果，则将其发送到结果队列等待 Result Worker 处理\n",
    "\n",
    "Scheduler接收到新的抓取任务，然后查询数据库，判断其如果是新的抓取任务或者是需要重\n",
    "试的任务就继续进行调度，然后将其发送回Fetcher进行抓取\n",
    "\n",
    "不断重复以上工作，直到所有的任务都执行完毕，抓取结束\n",
    "\n",
    "抓取结束后，程序会回调 on_finished()方法，这里可以定义后处理过程\n",
    "\n",
    "如果要快速实现一个页面的抓取，推荐使用 pyspider,开发更加便捷,如果要应对反爬程度很强、超大规模的抓取，推荐使用 Scrapy\n",
    "\n",
    "## 2. 应用\n",
    "\n",
    "安装教程以及相关问题解决：[https://blog.csdn.net/qq_40765537/article/details/105457499](https://blog.csdn.net/qq_40765537/article/details/105457499)\n",
    "\n",
    "相关配置完后，下面需要在命令行cmd执行命令启动pyspider"
   ]
  },
  {
   "cell_type": "raw",
   "source": [
    "pyspider all"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% raw\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "这样可以启动所有组件，包括 PhantomJS ResultWorker Processer Fetcher Scheduler\n",
    "WebUI这些都是pyspider运行必备的组件 最后一行输出提示运行在5000端口上\n",
    "\n",
    "进入url:[http://localhost:5000/](http://localhost:5000/)\n",
    "\n",
    "![](PySpiderUI.jpg)\n",
    "\n",
    "目标爬取网站：去哪儿网[https://www.qunar.com/](https://www.qunar.com/)\n"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "进入后可以看见右侧的自动生成的爬取代码\n"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% md\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "#!/usr/bin/env python\n",
    "# -*- encoding: utf-8 -*-\n",
    "# Created on 2020-08-20 11:36:00\n",
    "# Project: Baidu\n",
    "\n",
    "from pyspider.libs.base_handler import *\n",
    "\n",
    "class Handler(BaseHandler):\n",
    "    crawl_config = {\n",
    "    }\n",
    "\n",
    "    @every(minutes=24 * 60)\n",
    "    def on_start(self):\n",
    "        self.crawl('http://www.baidu.com', callback=self.index_page)\n",
    "\n",
    "    @config(age=10 * 24 * 60 * 60)\n",
    "    def index_page(self, response):\n",
    "        for each in response.doc('a[href^=\"http\"]').items():\n",
    "            self.crawl(each.attr.href, callback=self.detail_page)\n",
    "\n",
    "    @config(priority=2)\n",
    "    def detail_page(self, response):\n",
    "        return {\n",
    "            \"url\": response.url,\n",
    "            \"title\": response.doc('title').text(),\n",
    "        }"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "这里的 Handler 就是 pyspider 爬虫的主类，我们可以在此处定义爬取 解析、存储的逻辑\n",
    "\n",
    "整个爬虫的功能只需要一个 Handler 即可完成\n",
    "\n",
    "接下来我们可以看到crawl_config属性。我们可以将本项目的所有爬取配置统一定义到这\n",
    "里，如定义Headers、设置代理等，配置之后全局生效\n",
    "\n",
    "然后， on_start （）方法是爬取入口，初始的爬取请求会在这里产生，该方法通过调用crawl() 方法\n",
    "即可新建一个爬取请求，第 个参数是爬取的 URL ，这里自动替换成我们所定义的URL crawl()方法\n",
    "还有 个参数 callback ，它指定了这个页面爬取成功后用哪个方法进行解析，代码中指定为 index_page()\n",
    "方法，即如果这个 URL 对应的页面爬取成功了，那 Response 将交给 index_page()方法解析\n",
    "\n",
    "进入pyspider时，首先弹出界面：![](Logpyspider.jpg)\n",
    "\n",
    "project name 是你项目的名称 start url 是你要爬取的链接，分别输入以后，再次点击右下角的create，完成创建\n",
    "\n",
    "随后进入可以看到下面的界面：![](ContentSpider.jpg)\n",
    "\n",
    "整个右边部分，分为这些项目：\n",
    "1.右上角的save 是保存按钮，更改代码之后几点保存\n",
    "2.左上角的#后面的是你创建的一些参数，如创建时间，爬取域名，编码格式等\n",
    "3.下面的代码部分是创建项目自动生成的，你可以添加一些你自己的东西，我们在这里完成整个项目的爬取、解析、以及保存等工作\n",
    "\n",
    "crawl_config：项目的所有爬取配置统一定义到这里，如定义 Headers 、设置代理等，配置之后全局生效\n",
    "\n",
    "@every：设置定时爬取的时间\n",
    "\n",
    "on_start：爬取入口，初始的爬取请求会在这里产生\n",
    "\n",
    "self.crawl：该方法是爬取的主方法，被on_start调用，即可新建一个爬取请求，这里面有这么几个参数第一个参数是爬取的 URL ，这里自动替换成我们所定义的 URL ，还有 个参数 callback ，它指定了这个页面爬取成功后用哪个方法进行解析，代码中指定为 Idex_page()方法，即如果这个 URL 对应的页面爬取成功了，那 Response 将交给 index_page （）方法解析\n",
    "\n",
    "index_page：方法接收这个 Response 参数，Response 对接了 pyquery 我们直接调用 doc()方法传入相应的css 选择器，就可以像 query 一样解析此页面，代码中默认是 a[href\"=\"http ”］，也就是说该方法解析了页面的所有链接，然后将链接遍历，再次调用了 crawl （）方法生成了新的爬请求，同时再指定了 callback为detail_page ，意思是说这些页面爬取成功了就调用 detail_page方法解析这里，index_page （）实现了两个功能，一是将爬取的结果进行解析， 二是生成新的爬取请求\n",
    "\n",
    "detail age：接收 Response 作为参数该方法抓取的就是详情页的信息，不会生成新的请求，只对 Response 对象做解析，解析之后将结果以字典的形式返回，将结果保存到数据库\n",
    "\n",
    "可能遇到的问题：\n",
    "1. SSL 验证不通过报错\n",
    "\n",
    "报错名：HTTP 599: SSL certificate problem: unable to get local issuer certificate\n",
    "\n",
    "解决方案：对右边的文本设计的crawl方法进行重写，添加参数validate_cert=False，以忽略证书的要求"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "self.crawl(each.attr.href, callback=self.detail_page,validate_cert=False)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "2.爬虫翻页设置：\n",
    "\n",
    "在index_page()的方法中改写，实现"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "next = response.doc(\".pagnation.nav.next\").attr.href\n",
    "self.crawl(next,callback=self.index_page,validate_cert=False)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "3. 处理获取图片：\n",
    "\n",
    "在index_page()的方法中改写，实现\n"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "source": [
    "def index_page(self, response):\n",
    "        for each in response.doc('a[href^=\"http\"]').items():\n",
    "            self.crawl(each.attr.href, callback=self.detail_page,validate_cert=False,fetch_type='js')\n",
    "        next = response.doc('.next').attr.href\n",
    "        self.crawl(next,callback=self.index_page,validate_cert=False)"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "4. 提取详情页面的信息(下面是例子)："
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "source": [
    "def detail_page(self, response):\n",
    "        return {\n",
    "            \"url\": response.url,\n",
    "            \"title\": response.doc('#booktitle').text(),\n",
    "            \"date\": response.doc('.when .data').text(),\n",
    "            \"day\": response.doc('.howlong .data').text(),\n",
    "            \"who\": response.doc('.who .data').text(),\n",
    "            \"image\": response.doc('.cover_img').attr.src\n",
    "        }"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "上面代码模拟实现一个页面链接、标题、出行日期、出行天数、任务、头图信息等的字典(json格式)\n",
    "\n",
    "5.爬取\n",
    "\n",
    "返回爬虫的主页面，将爬虫的 status 置成 DEBUG/RUNNING ，点击右侧的 Run 按钮即可\n",
    "始爬取\n",
    "\n",
    "![](RunSpider.JPG)\n",
    "\n",
    "在最左侧我们可以定义项目的分组，以方便管理 rate/burst 表当前的爬取速率，rate代表\n",
    "1s发出多少个请求\n",
    "\n",
    "burst当于流量控制中的令牌桶算法的令牌数， rate/burst 设置的越大，爬取速率\n",
    "越快，当然连率需要考虑本机性能和爬取过快被封的问题\n",
    "\n",
    "process 中的 5m lh ld 指的是最近分、小时、天内的请求情况、all代表所有的请求情况\n",
    "\n",
    "请求由不同颜色表示，蓝色的代表等待被执行的请求，\n",
    "\n",
    "绿色的代表成功的请求，黄色的代表请求失败后等待重试的请求，红色的代表失败次数过\n",
    "多导致被忽略的请求。\n",
    "\n",
    "这样可以直观知道爬取的进度和请求情况\n",
    "\n",
    "点 Active Tasks ，即可查看最近请求的详细状况:\n",
    "\n",
    "![](ActivaTasks.JPG)\n",
    "\n",
    "点击Results查看所有结果：\n",
    "\n",
    "![](ResultSpider.JPG)\n",
    "\n",
    "## PySpider 指令讲解\n",
    "\n",
    "之前讲解了PySpider的界面的操作，下面简要介绍相关指令\n"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "pyspider [OPTIONS] COMMAND [ARGS]"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% raw\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "所有的命令参数(包括组件子命令)都可以通过-c由一个json文件配置\n",
    "\n",
    "例如:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "{\n",
    "  \"taskdb\": \"mysql+taskdb://username:password@host:port/taskdb\",\n",
    "  \"projectdb\": \"mysql+projectdb://username:password@host:port/projectdb\",\n",
    "  \"resultdb\": \"mysql+resultdb://username:password@host:port/resultdb\",\n",
    "  \"message_queue\": \"amqp://username:password@host:port/%2F\",\n",
    "  \"webui\": {\n",
    "    \"username\": \"some_name\",\n",
    "    \"password\": \"some_passwd\",\n",
    "    \"need-auth\": true\n",
    "  }"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "其中, OPTIONS 全局配置,对子命令有效,它可以指定如下参数"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "-c, --config FILENAME   指定配置文件名称,一般为json格式\n",
    "--logging-config TEXT   日志配置文件名称 ,默认 : pyspider/pyspider/logging.conf\n",
    "--debug 开启调试模式\n",
    "--queue-maxsize INTEGER 队列的最大长度\n",
    "--taskdb TEXT   taskdb 的数据库连接字符串,默认: sqlite\n",
    "--projectdb TEXT    projectdb 的数据库连接字符串,默认: sqlite\n",
    "--resultdb TEXT resultdb 的数据库连接字符串,默认: sqlite\n",
    "--message-queue TEXT    消息队列连接字符串,默认:multiprocessing.Queue\n",
    "--phantomjs-proxy TEXT  PhantomJS 使用的代理, ip:port 的形式\n",
    "--data-path TEXT    数据库存放的路径\n",
    "--version   pyspider 的版本\n",
    "--help  显示帮助信息"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "单独配置某一组件:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "pyspider scheduler [OPTIONS]\n",
    "pyspider bench [OPTIONS]\n",
    "pyspider fetcher [OPTIONS]\n",
    "pyspider processor [OPTIONS]\n",
    "pyspider webui [OPTIONS]"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "crawl方法参考文献：[http://docs.pyspider.org/en/latest/apis/self.crawl/](http://docs.pyspider.org/en/latest/apis/self.crawl/)\n",
    "\n",
    "url:url 是爬取时的 URL ,可以定义为单个 URL 字符串,也可以定义成 URL 列表\n",
    "\n",
    "callback:callback 是回调函数,指定了该 URL 对应的响应内容用哪个方法来解析\n",
    "\n",
    "age:age是任务的有效时间.如果某个任务在有效时间内且已经被执行,则它不会重复执行,默认的有效时间为 10 天\n",
    "\n",
    "priority:priority 是爬取任务的优先级,其值默认是 0, priority 的数值越大,对应的请求会越优先被调度\n",
    "\n",
    "exetime:exetime 参数可以设置定时任务,其值是时间戳,默认是 0 ,即代表立即执行.例如exetime=time.time()+30*60\n",
    "\n",
    "itag:itag 参数设置判定网页是存发生变化的节点值,在爬取时会判定次当前节点是否和上次爬取到的节点相同 。如果节点相同,则证明页面没有更新,就不会重复爬取.例如itag=i tem.find(’.update-time’).text()\n",
    "\n",
    "auto_recrawl:当开启(True)时,爬取任务在过期后会重新执行,循环时间即定义的 age 时间长度\n",
    "\n",
    "method:method 是 HTTP 请求方式,它默认是 GET\n",
    "\n",
    "params:可以方便地使用 params 来定义 GET 请求参数.例如:params={‘a’:123, ‘b’: ‘c’}\n",
    "\n",
    "data:data 是 POST 表单数据.当请求方式为POST时,我们可以通过此参数传递表单数据.例如:data={‘a’:123, ‘b’: ‘c’}\n",
    "\n",
    "files:files 是上传的文件,需要指定文件名.例如:files ={field: {filename: ‘content’}\n",
    "\n",
    "user_agent:user_agent 是爬取使用的 User-Agent 。\n",
    "\n",
    "headers:headers 是爬取时使用的 Headers ,即 Request Headers\n",
    "\n",
    "cookies:cookies 是爬取时使用的 Cookies ,为字典格式 。\n",
    "\n",
    "connect timeout:connect timeout 是在初始化连接时的最长等待时间,它默认是 20 秒\n",
    "\n",
    "timeout:timeout 是抓取网页时的最长等待时间,它默认是 120 秒。\n",
    "\n",
    "allow redirects:allow redirects 确定是否自动处理重定向,它默认是 True 。\n",
    "\n",
    "validate cert:validate cert 确定是否验证证书,此选项对 HTTPS 请求有效,默认是 True 。\n",
    "\n",
    "proxy:proxy 是爬取时使用的代理,它支持用户名密码的配置,格式为 username:password@hostname:port\n",
    "\n",
    "fetch_type:fetch_type 开启 PhantomJS 渲染.fetch_type = ‘js’\n",
    "\n",
    "js_script:js_script 是页面加载完毕后执行的 JavaScript 脚本\n",
    "\n",
    "js_run_at:JavaScript脚本运行的位置,是在页面节点开头还是结尾 , 默认是结尾, 即 document-end\n",
    "\n",
    "js_viewport_width /js_viewport_height:是 JavaScript 渲染页面时的窗口大小\n",
    "\n",
    "load_images:在加载 JavaScript 页面时确定是否加载图片,它默认是否\n",
    "\n",
    "save:参数非常有用,可以在不同的方法之间传递参数.例如设置save={‘page’:1},在回调函数里可以通过 response变量的save字段接收到这些参数值response.save[‘page’]\n",
    "\n",
    "cancel:是取消任务,如果一个任务是 ACTIVE 状态的, 则需要将 force_update设置为 True\n",
    "\n",
    "force_ update:即使任务处于 ACTIVE 状态,那也会强制更新状态\n",
    "\n",
    "如果要配置 pyspide WebUI 的访问认证，可以新建pyspider.json内容如下所示："
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "{\n",
    "    \"webui\":{\n",
    "    \"username\"·\"root\",\n",
    "    \"password\":\"123456\",\n",
    "    \"need-auth\": true\n",
    "    }\n",
    "}"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "这样我们通过在启动时指定配置文件来配置 pyspider WebUI 的访问认证，\n",
    "用户名为 root ，密码123456 ，命令行执行代码："
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "pyspider -c pyspider.json all"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "任务区分：\n",
    "\n",
    "在 pyspider 判断两个任务是否是重复的是使用的是该任务对应的 URL 的 MD5 值作为任务的唯一ID ,如果ID相同,那么两个任务就会判定为相同,其中一个就不会爬取了。很多情况下请求的链接可能是同一个,但是POST的参数不同.这时可以重写get_taskid()方法,改变这个 ID 的计算方式来实现不同任务的区分."
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% md\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "import json\n",
    "from pyspider.libs.utils import md5string\n",
    "def get_taskid(self, task):\n",
    "    return md5string(task['url']+ json.dumps(task['fetch'].get ('data','')))"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "利用URL 和 POST 的参数来生成ID。\n",
    "这样一来,即使URL相同,但是POST的参数不同,\n",
    "两个任务的ID就不同,它们就不会被识别成重复任务。\n",
    "\n",
    "### 全局配置\n",
    "使用crawl_config指定全局的配置(字典)，配置中的参数和crawl()方法创建任务时的参数合并\n"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "class Handler(BaseHandler):\n",
    "    crawl_config = {\n",
    "        'headers':{\n",
    "            'User-Agent':'GoogleBot',\n",
    "        }\n",
    "    }"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "### 定时爬取\n",
    "\n",
    "定时爬取（请求过期时间内，不执行爬取）设定时间间隔\n",
    "\n",
    "例如：下面代码实现每日一次爬取"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% md\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "@every(minutes=24*60)\n",
    "def on_start(self):\n",
    "    for url in urllist:\n",
    "        self.crawl(url,callback=self.index_page)"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "注意有效时间要小于重复时间，否则更短\n",
    "\n",
    "## 项目状态"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "raw",
   "source": [
    "TODO：它是项目刚刚被创建还未实现时的状态 。\n",
    "STOP：如果想停止某项目的抓取,可以将项目的状态设置为 STOP 。\n",
    "CHECKING：正在运行的项目被修改后就会变成 CHECKING 状态,项目在中途出错需要调整\n",
    "的时候会遇到这种情况 。\n",
    "DEBUG/RUNNING：这两个状态对项目的运行没有影响,状态设置为任意一个,项目都可以\n",
    "运行,但是可以用二者来区分项目是否已经测试通过 。\n",
    "注：只有设为DEBUG/RUNNING之后，才可以在【actions】中点击【run】按钮\n",
    "PAUSE：当爬取过程中出现连续多次错误时,项目会自动设置为 PAUSE 状态,并等待一定时间后继续爬取"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 进度条：\n",
    "\n",
    "5m 1h 1d 表示最近5分、1小时、1天以内的请求情况，all表示所有情况\n",
    "\n",
    "蓝色表示等待被执行任务，绿色表示成功的任务，黄色表示失败后等待重试任务，红色表示失败过多被忽略的任务\n",
    "\n",
    "### 删除：\n",
    "\n",
    "删除时，将项目状态改成STOP，分组名称设置成delete等待24小时自动删除。"
   ],
   "metadata": {
    "collapsed": false
   }
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}